---
description: Компонент для выбора значения из выпадающего списка.
---

<Overview group="forms">

# CustomSelect [tag:component]

Компонент для выбора значения из выпадающего списка.
Поддерживает широкие возможности по изменению внешнего вида, поиск по опциям и пользовательский рендеринг опций.

Связанные компоненты:

- [`Select`](/components/select)

</Overview>

> Важно: для отображения невыбранного состояния нужно использовать `value=null` вместо `undefined`.
>
> `undefined` ипользуется только для неконтролируемого компонента.

<Playground style={{ maxWidth: 270 }}>

```jsx
<CustomSelect
  options={[
    { value: 0, label: 'Москва' },
    { value: 1, label: 'Санкт-Петербург' },
    { value: 2, label: 'Новосибирск' },
    { value: 3, label: 'Йошкар-Ола' },
  ]}
  placeholder="Выберите город"
  allowClearButton
/>
```

</Playground>

## Режим работы

Компонент поддерживает работу как в неконтролируемом режиме, так и контролируемом. Это стандартное поведение
React-компонентов, прочитать про это можно в [документации React](https://react.dev/reference/react-dom/components/input#controlling-an-input-with-a-state-variable).

Для использования неконтролируемого режима достаточно просто _не_ передавать `value` или передавать `defaultValue`, если
требуется задать значение по умолчанию.
Для контролируемого режима используйте связку свойств `value`/`onChange` для задания значения и его изменения.

```jsx
const colors = [
  {
    value: 'red',
    label: 'Красный',
  },
  {
    value: 'green',
    label: 'Зелёный',
  },
  {
    value: 'blue',
    label: 'Синий',
  },
];

// Неконтролируемое состояние
<CustomSelect options={colors} defaultValue="red" />;

// Контролируемое состояние
const [color, setColor] = React.useState('red');

<CustomSelect options={colors} value={color} onChange={(_, newColor) => setColor(newColor)} />;
```

## Состояния

### `disabled`

Свойство `disabled` блокирует взаимодействие с компонентом и добавляет визуальную индикацию недоступности.

<Playground style={{ maxWidth: 270 }}>

```jsx
<CustomSelect options={[{ value: 'red', label: 'Красный' }]} defaultValue="red" disabled />
```

</Playground>

## Валидация

Свойство `status` используется для визуализации валидации компонента - некорректности заполненного поля (значение `"error"`)
или успешной валидации (значение `"valid"`).

> Если вы используете `CustomSelect` вместе с [`FormItem`](/components/form-item), вам достаточно указать свойство `status` только у
> [`FormItem`](/components/form-item).

<Playground style={{ maxWidth: 270 }}>

```jsx
<CustomSelect options={[{ value: 'red', label: 'Красный' }]} defaultValue="red" status="error" />
<CustomSelect
  options={[{ value: 'green', label: 'Зеленый' }]}
  defaultValue="green"
  status="valid"
/>
```

</Playground>

## Кастомизация

### Визуальное оформление

С помощью свойства `renderOption` можно влиять на отображение конкретного элемента выпадающего списка.
Вы можете изменить стандартный компонент [`CustomSelectOption`](#customselectoption) или использовать свой компонент.

<Playground style={{ maxWidth: 270 }}>

```jsx
<CustomSelect
  options={[
    { value: 0, label: 'Москва', country: 'Россия' },
    { value: 1, label: 'Санкт-Петербург', country: 'Россия' },
    { value: 2, label: 'Новосибирск', country: 'Россия' },
    { value: 3, label: 'Йошкар-Ола', country: 'Россия' },
  ]}
  placeholder="Выберите город"
  allowClearButton
  renderOption={({ option, ...restProps }) => (
    <CustomSelectOption {...restProps} description={option.country} />
  )}
/>
```

</Playground>

## Кастомное поведение при поиске

Пример показывает, как реализовать кастомное поведение поиска: при вводе сохраняется локальный `query`, при отсутствии совпадений в списке появляется опция «Добавить пользователя …»,
её выбор добавляет новый элемент в `options` — вместе с пользовательским рендером опций это даёт удобный UX для создания элементов на лету.

<Playground style={{ maxWidth: 270 }}>

```jsx
const users = [
  {
    id: 58690866,
    first_name: 'Эльдар',
    last_name: 'Мухаметханов',
    screen_name: 'e.muhamethanov',
  },
  {
    id: 1771114,
    first_name: 'Ином',
    last_name: 'Мирджамолов',
    screen_name: 'inomdzhon',
  },
  {
    id: 16790455,
    first_name: 'Вика',
    last_name: 'Жижонкова',
    screen_name: 'BlackySoul',
  },
  {
    id: 117253521,
    first_name: 'Даниил',
    last_name: 'Суворов',
    screen_name: 'SevereCloud',
  },
  {
    id: 99539515,
    first_name: 'Никита',
    last_name: 'Денисов',
    screen_name: 'qurle',
  },
  {
    id: 296221155,
    first_name: 'Алексей',
    last_name: 'Зайцев',
    screen_name: 'Zaycevq',
  },
];

const getUsers = (usersArray) =>
  usersArray.map((user) => ({
    label: `${user.first_name} ${user.last_name}`,
    value: `${user.id}`,
    description: user.screen_name,
  }));

const [value, setValue] = React.useState('');
const [query, setQuery] = React.useState('');
const [newUsers, setNewUsers] = React.useState([...getUsers(users)]);

const customSearchOptions = () => {
  const options = [...newUsers];
  if (query.length > 0 && !options.find((user) => user.value === query || user.label === query)) {
    options.unshift({
      label: `Добавить пользователя ${query}`,
      value: '0',
    });
  }
  return options;
};

const onCustomSearchChange = (_, newValue) => {
  if (newValue === '0') {
    setNewUsers([...newUsers, { label: query, value: query }]);
    setValue(query);
  } else {
    setValue(newValue);
  }
  setQuery('');
};

const onCustomSearchInputChange = (e) => {
  setQuery(e.target.value);
};

return (
  <CustomSelect
    value={value}
    placeholder="Введите имя пользователя"
    searchable
    options={customSearchOptions()}
    onInputChange={onCustomSearchInputChange}
    renderOption={({ option, ...restProps }) => (
      <CustomSelectOption
        style={option.value === '0' ? { color: 'var(--vkui--color_text_accent)' } : {}}
        {...restProps}
      >
        {option.label}
      </CustomSelectOption>
    )}
    onChange={onCustomSearchChange}
  />
);
```

</Playground>

## Кастомный алгоритм поиска

Демонстрация кастомного алгоритма поиска иллюстрирует передачу `filterFn` для гибкого соответствия (по `label` и `description`),
поддержку `searchable` и кастомного рендера опций — полезно, когда нужно искать по нескольким полям или использовать нестандартные критерии.

<Playground style={{ maxWidth: 270 }}>

```jsx
const cities = [
  {
    label: 'Санкт-Петербург',
    description: 'Россия',
    value: '0',
  },
  {
    label: 'Москва',
    description: 'Россия',
    value: '1',
  },
  {
    label: 'Новосибирск',
    description: 'Россия',
    disabled: true,
    value: '2',
  },
  {
    label: 'Нью-Йорк',
    description: 'США',
    value: '3',
  },
  {
    label: 'Чикаго',
    description: 'США',
    value: '4',
  },
];

const customSearchFilter = (value, option) =>
  option.label.toLowerCase().includes(value.toLowerCase()) ||
  option.description.toLowerCase().includes(value.toLowerCase());

return (
  <CustomSelect
    id="custom-search-algo-select-id"
    placeholder="Введите название города или страны"
    searchable
    filterFn={customSearchFilter}
    renderOption={({ option, ...restProps }) => (
      <CustomSelectOption {...restProps} description={option.description} />
    )}
    options={cities}
  />
);
```

</Playground>

## Асинхронная загрузка списка

Асинхронный пример показывает подгрузку опций с задержкой/дебаунсом, управление состояниями `fetching`/`remoteQuery`,
минимальную длину запроса, очистку таймаутов и возможность заменить содержимое выпадающего списка (например, подсказкой «введите хотя бы три символа») — стандартный шаблон для работы с удалёнными API.

<Playground style={{ maxWidth: 600 }}>

```jsx
const users = [
  {
    id: 58690866,
    first_name: 'Эльдар',
    last_name: 'Мухаметханов',
    screen_name: 'e.muhamethanov',
  },
  {
    id: 1771114,
    first_name: 'Ином',
    last_name: 'Мирджамолов',
    screen_name: 'inomdzhon',
  },
  {
    id: 16790455,
    first_name: 'Вика',
    last_name: 'Жижонкова',
    screen_name: 'BlackySoul',
  },
  {
    id: 117253521,
    first_name: 'Даниил',
    last_name: 'Суворов',
    screen_name: 'SevereCloud',
  },
  {
    id: 99539515,
    first_name: 'Никита',
    last_name: 'Денисов',
    screen_name: 'qurle',
  },
  {
    id: 296221155,
    first_name: 'Алексей',
    last_name: 'Зайцев',
    screen_name: 'Zaycevq',
  },
];

const getUsers = (usersArray) =>
  usersArray.map((user) => ({
    label: `${user.first_name} ${user.last_name}`,
    value: `${user.id}`,
    description: user.screen_name,
  }));

const [searchable, setSearchable] = React.useState(false);
const [remoteQuery, setRemoteQuery] = React.useState('');
const [fetching, setFetching] = React.useState(false);
const [remoteUsers, setRemoteUsers] = React.useState([]);

let timeout;

const cleanFetchingTimeout = () => {
  if (timeout) {
    clearTimeout(timeout);
  }
};

const fetchRemoteUsers = () => {
  setFetching(true);
  timeout = setTimeout(() => {
    setRemoteUsers([...getUsers(users)]);
    setFetching(false);
    cleanFetchingTimeout();
  }, 1500);
};

const searchRemoteUsers = (e) => {
  const _remoteQuery = e.target.value;
  cleanFetchingTimeout();
  setRemoteQuery(_remoteQuery);

  if (_remoteQuery.length < 3) {
    setRemoteUsers([]);
    setFetching(false);
  } else {
    fetchRemoteUsers();
  }
};

const clearRemoteUsers = () => {
  setRemoteUsers([]);
  setRemoteQuery('');
  cleanFetchingTimeout();
};

const renderDropdown = ({ defaultDropdownContent }) => {
  if (remoteQuery.length < 3) {
    return (
      <Text style={{ padding: 12, color: 'var(--vkui--color_text_secondary)' }}>
        Нужно ввести хотя бы три символа
      </Text>
    );
  }
  return defaultDropdownContent;
};

React.useEffect(() => {
  return () => cleanFetchingTimeout();
}, []);

return (
  <FormLayoutGroup mode="horizontal" style={{ width: '100%' }}>
    <FormItem style={{ flexGrow: 1, flexShrink: 1 }}>
      <CustomSelect
        aria-label="Пользователь"
        options={remoteUsers}
        searchable={searchable}
        placeholder={searchable ? 'Введите имя пользователя' : 'Не выбран'}
        disabled={searchable && fetching}
        onInputChange={searchable ? searchRemoteUsers : undefined}
        onOpen={searchable ? undefined : remoteUsers.length === 0 && fetchRemoteUsers}
        onClose={() => {
          console.log('CLOSED [async CustomSelect]');
        }}
        fetching={fetching}
        renderDropdown={searchable && !fetching && renderDropdown}
      />
    </FormItem>

    <FormItem style={{ flexBasis: '200px', flexGrow: 0 }}>
      <Checkbox
        onChange={(e) => {
          setSearchable(e.target.checked);
          clearRemoteUsers();
        }}
      >
        Использовать поиск
      </Checkbox>
    </FormItem>
  </FormLayoutGroup>
);
```

</Playground>

## CustomSelectOption [#custom-select-option]

Универсальный компонент для отрисовки одного из значений в выпадающем списке.
Передавайте данный компонент в свойство `renderOption`, для кастомизации значений выпадающего списка.
Помимо `CustomSelect` используется и в `ChipsSelect`.

<Playground style={{ maxWidth: 270 }}>
  ```jsx
  <CustomSelectOption before={<Avatar initials="ИИ" size={20} aria-hidden />} after={<Icon16Pin />}>
    Иван Иванов
  </CustomSelectOption>
  ```
</Playground>

Для реализации многоуровневых `CustomSelectOption` используйте свойство `hierarchy`,
которое позволяет создать нужный отступ в зависимости от уровня вложенности.

<Playground style={{ maxWidth: 270 }}>
  ```jsx
  <CustomSelectOption>Диск</CustomSelectOption>
  <CustomSelectOption hierarchy={1}>Папка</CustomSelectOption>
  <CustomSelectOption hierarchy={2}>Файл 1</CustomSelectOption>
  <CustomSelectOption hierarchy={2} selected>
    Файл 2
  </CustomSelectOption>
  ```
</Playground>

## Тестирование (e2e) [#e2e]

Для поиска компонента и его элементов на странице cуществует ряд вспомогательных свойств:

По умолчанию все `data-*` атрибуты попадают на `<input>` компонента, это значит, что при передаче
`data-testid` или `data-test-id` идентификатор попадёт `<input>`.
Доступ к полю ввода может быть полезен для:

- получения текста, введённого пользователем при поиске опций, в режиме `searchable`;
- получения информации о наличии фокуса на компоненте;
- симуляции работы с компонентом с клавиатуры.

Используйте `labelTextTestId`:

- для симуляции работы с компонентом с помощью мыши, тач-устройства;
- для получения текста выбранной в данный момент опции.

Для взаимодействия с кнопкой очистки состояния компонента, которая появляется,
если `CustomSelect` имеет свойство `searchable` и пользователь выбрал опцию, используйте свойство `clearButtonTestId`.

`CustomSelect` внутри себя хранит невидимый `<select>`, для того, чтобы `CustomSelect` можно было использовать внутри формы.
Для получения доступа к `<select>` используйте свойство `nativeSelectTestId`. Полезно для доступа к значению `value`,
выбранной в данный момент опции.

Все перечисленные выше свойства устанавливают аттрибут `data-testid` у соответствующих элементов. Учитывайте это при построении селекторов.

## Кастомизация

Компонент поддерживает свойство `slotProps`, которое даёт возможность прокинуть свойство в некоторые внутренние элементы.
Это удобно для добавления кастомных классов, data-атрибутов, aria-атрибутов, обработчиков событий, доступов к элементам через `getRootRef` и других расширений, не влияя на внешний API компонента.

<Playground style={{ maxWidth: 270 }}>
  ```jsx
  const inputRef = React.useRef();
  const selectRef = React.useRef();

  return (
    <CustomSelect
      options={[
        { value: 0, label: 'Москва' },
        { value: 1, label: 'Санкт-Петербург' },
        { value: 2, label: 'Новосибирск' },
        { value: 3, label: 'Йошкар-Ола' },
      ]}
      placeholder="Выберите город"
      className="my-root-class"
      data-testid="select-root"
      id="custom-select-input-id"
      slotProps={{
        root: {
          id: 'select-root-id',
        },
        input: {
          className: 'my-input-class',
          getRootRef: inputRef,
        },
        select: {
          className: 'my-select-class',
          'data-test-id': 'select',
          getRootRef: selectRef,
        }
      }}
    />
  )
  ```
</Playground>

## Доступность (a11y) [#a11y]

Следуйте рекомендациям [доступности компонента `Select`](/components/select#a11y).

### Индикация ассинхронной загрузки

Для того, чтобы уведомить пользователей скринридеров о том, что идет загрузка списка опций использутся специальные текстовые метки совместно со свойством `fetching`.
Текст этих меток можно переопределить с помощью свойств `fetchingInProgressLabel` и `fetchingCompletedLabel`. По умолчанию они соответственно: `"Список опций загружается..."` и `"Опций загружено: ${options.length}"`

### Проблема определения выбранной опции

В текущей реализации (`v7`) компонент `CustomSelect` имеет проблему с доступностью в некоторых скринридерах (например, `NVDA`): пользователь не может узнать, какой вариант выбран.
Это происходит из-за сброса значения в `input` при потере фокуса.

Такое поведение было добавлено для ускорения ввода новых значений, но оно негативно влияет на доступность.

#### Решение

Мы изменили логику работы компонента. Чтобы включить исправленное поведение, используйте флаг `accessible`. Он введён для сохранения обратной совместимости в минорных обновлениях.

#### Рекомендация

С `v8` этот флаг теперь включен по умолчанию. Не рекомендуется отключать этот флаг, так как с `v9` этот флаг будет удален.

## Свойства и методы [#api]

<PropsTable name={["CustomSelect", "CustomSelectOption"]}>
### CustomSelect [#custom-select-api]

### CustomSelectOption [#custom-select-option-api]

</PropsTable>
